#include "audio.h"
#include "audio_input_wave.h"

#include <iostream>
#include <fstream>
#include <cassert>
#include <limits>

using namespace krryn::audio;

namespace krryn{
namespace audio_imp{
	class wave_chunk_header{
	public:
		char m_ID[4];
		int_32 m_Size;
	};
    
	class wave_chunk_riff{
	public:
		wave_chunk_header m_Header;
		char m_Format[4];
	};
    
	class wave_chunk_fmt{
	public:
		wave_chunk_header m_Header;
		int_16 m_AudioFormat;
		int_16 m_NumChannels;
		int_32 m_SampleRate;
		int_32 m_ByteRate; // bytes per second
		int_16 m_BlockAlign;
		int_16 m_BitsPerSample;
	};

	class wave_chunk_data{
	public:
		wave_chunk_header m_Header;
	};
    
	enum audio_format{
		fmt_pcm = 1,
	};

	template<typename T>
	static T read_and_validate(std::ifstream &a_Input, const std::string &a_HeaderId){
		T l_Chunk;
		a_Input.read((char*)&l_Chunk, sizeof(T));
		
		// assume first few bytes are a wave_chunk_header
		wave_chunk_header *l_Header = (wave_chunk_header*)&l_Chunk;

		if(cmp(l_Header->m_ID, a_HeaderId.c_str())){
			validate(l_Chunk);
			return l_Chunk;
		}else{
			throw audio::exception(a_HeaderId + " chunck expected");
		}
	}

	static bool cmp(const char *a_Left, const char *a_Right){
		for(int i = 0; i < 4; i++)
			if(*a_Left++ != *a_Right++) return false;
		return true;
	}

	static void validate(const wave_chunk_riff &a_Riff){
		if(!cmp(a_Riff.m_Format, "WAVE"))
			throw exception("Not a wave file!");
	}

	static void validate(const wave_chunk_fmt &a_Fmt){
		if(a_Fmt.m_AudioFormat != fmt_pcm) 
			throw exception("Can only read PCM files!");

		if(a_Fmt.m_Header.m_Size != 16)
			throw exception("Invalid chunk header size!");

		if(a_Fmt.m_NumChannels > 2) 
			throw exception("Can't read more than two channels!");

		if(a_Fmt.m_ByteRate != a_Fmt.m_SampleRate * a_Fmt.m_NumChannels * a_Fmt.m_BitsPerSample / 8)
			throw exception("Invalid byte rate!");

		if(a_Fmt.m_BlockAlign != a_Fmt.m_NumChannels * a_Fmt.m_BitsPerSample / 8)
			throw exception("Invalid block align!");

		if(a_Fmt.m_BitsPerSample != 16)
			throw exception("Can only read 16 bits per sample!");
	}

	static void validate(const wave_chunk_header &a_Header){}
	static void validate(const wave_chunk_data &a_Header){}
}
}

using namespace krryn::audio_imp;

input_wave::input_wave(const std::string &a_Filename)
	: m_Filename(a_Filename),
	  m_Init(false), 
	  m_TotalSize(0), 
	  m_TotalRead(0), 
	  m_MaxBlockSize(100 Mb),
	  m_Input(0)
{
	init();
}

input_wave::~input_wave(){}

bool input_wave::more(){
	return m_Input->good() &&  m_TotalRead < m_TotalSize;
}

sample::ptr input_wave::next(int a_MilliSeconds){
	sample::ptr l_Sample;

	// byte rate is in bytes per second
	int l_BlockSize = m_Channels * m_ByteRate * a_MilliSeconds / 1000;

	if(l_BlockSize < 0 || l_BlockSize >= m_MaxBlockSize){
		throw audio::exception("Invalid block size!");
	}

	if(m_Input->eof() || !m_Input->good() || !more()){
		if(m_Input->is_open())
			m_Input->close();
		return l_Sample;
	}
	
	if(m_TotalRead < m_TotalSize){
		int l_DataSize = l_BlockSize / sizeof(int_16);
		int_16 *l_Data = new int_16[l_DataSize];

		l_Sample = new sample(m_SampleRate, m_Channels);
		l_Sample->allocate(l_DataSize);

		// Notice that we do l_DataSize * sizeof(int_16) instead of using
		// l_BlockSize directly. This is to prevent problems when l_BlockSize
		// is an odd number.
		m_Input->read(reinterpret_cast<char*>(l_Data), l_DataSize * sizeof(int_16));

		if(m_Input->fail() && !m_Input->eof()){
			throw audio::exception("IO Failure");
		}

		// adjust the number of samples we've read because we 
		// might have read less
		int l_Read = m_Input->gcount();
		l_Sample->set_count(l_Read / sizeof(int_16));

		// m_TotalRead is in bytes so don't divide
		m_TotalRead += l_Read;

		// convert to float scale to [-1, 1]
		sample::value_type *l_Buffer = l_Sample->buffer();
		for(int i = 0; i < l_Sample->count(); i++){
			*l_Buffer++ = static_cast<sample::value_type>(l_Data[i]) 
				/ (std::numeric_limits<int_16>::max)();
		}

		delete[] l_Data;
	}

	return l_Sample;
}

void input_wave::reset(){
	m_TotalRead = 0;
	m_Input->clear(std::ifstream::goodbit);
	m_Input->seekg(m_InitialPosition, std::ifstream::beg);
	assert(m_InitialPosition == m_Input->tellg());
}

void input_wave::init(){
	m_TotalSize = 0;
	m_TotalRead = 0;
	m_InitialPosition = 0;
	m_Input = std::auto_ptr<std::ifstream>(new std::ifstream(
		m_Filename.c_str(), std::fstream::in | std::fstream::binary));

	// find the file size
	m_Input->seekg(0, std::ifstream::end);
	std::ifstream::pos_type l_FileSize = m_Input->tellg();
	m_Input->seekg(0, std::ifstream::beg);

	if(!*m_Input) throw audio::exception("failed to open stream");

	wave_chunk_riff l_Riff = read_and_validate<wave_chunk_riff>(*m_Input, "RIFF");
	wave_chunk_fmt l_Fmt = read_and_validate<wave_chunk_fmt> (*m_Input, "fmt ");

	// skip all until we have a data block because there are some optional bytes
	// in the fmt chunk
	std::ifstream::streamoff l_Offset = (
		  sizeof(wave_chunk_fmt) 
		- l_Fmt.m_Header.m_Size 
		- sizeof(l_Fmt.m_Header.m_ID) // m_Size does NOT include the header ID
	) / 8; // offset is in bits, we need it in bytes

	m_Input->seekg(l_Offset, std::istream::cur);
	wave_chunk_data l_Data = read_and_validate<wave_chunk_data>(*m_Input, "data");

	// Validate file sizes
	if(    l_Riff.m_Header.m_Size != 36 + l_Data.m_Header.m_Size
		|| l_Riff.m_Header.m_Size + 8 != l_FileSize.seekpos()){
		throw audio::exception("Invalid RIFF header size");
	}

	// only leave this class in a correct state when everything has gone right
	m_TotalSize			= l_Data.m_Header.m_Size;
	m_InitialPosition	= m_Input->tellg();
	m_ByteRate			= l_Fmt.m_ByteRate;
	m_SampleRate		= l_Fmt.m_SampleRate;
	m_Channels			= l_Fmt.m_NumChannels;
	m_Init				= true;
}
